﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;

namespace Affinity.Synthesis.Collections
{
    /// <summary>
    /// Represents a source collection, a mapping from the source type to the target type, and a target collection consisting of the mapped elements.
    /// </summary>
    /// <typeparam name="TSource"></typeparam>
    /// <typeparam name="TTarget"></typeparam>
    public class MappedCollection<TSource, TTarget> : IEnumerable<TTarget>, INotifyCollectionChanged
    {
        /// <summary>
        /// Constructor accepting the mapping function.
        /// </summary>
        /// <param name="mapping"></param>
        public MappedCollection(Func<TSource, TTarget> mapping)
        {
            if (mapping == null) throw new ArgumentNullException("mapping");

            _Mapping = mapping;
        }

        /// <summary>
        /// Raised when the collection has changed.
        /// </summary>
        public event NotifyCollectionChangedEventHandler CollectionChanged;
        
        /// <summary>
        /// Gets a typed enumerator for the target collection.
        /// </summary>
        /// <returns></returns>
        public IEnumerator<TTarget> GetEnumerator()
        {
            return _TargetList.GetEnumerator();
        }

        /// <summary>
        /// Gets an untyped enumerator for the target collection.
        /// </summary>
        /// <returns></returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// Gets or sets the source items.
        /// </summary>
        public IEnumerable<TSource> SourceItems
        {
            get { return _SourceToTargetItemMap.Keys; }
            set { SetItems((value ?? new TSource[0]).ToArray()); }
        }
        
        /// <summary>
        /// Adds a source item.
        /// Executes the mapping function if and only if a cached value is not already present for the source item.
        /// If a cached target value is present for the source item, that value is used for the target element.
        /// The primary purpose of the caching is to prevent rerunning the mapping over all items when only a subset 
        /// has changed. 
        /// </summary>
        /// <param name="newSourceItem"></param>
        private void AddSourceItem(TSource newSourceItem)
        {
            TTarget newTargetItem;

            if (!_SourceToTargetItemMap.TryGetValue(newSourceItem, out newTargetItem))
            {
                newTargetItem = _Mapping(newSourceItem);
                _SourceToTargetItemMap[newSourceItem] = newTargetItem;
            }

            _TargetList.Add(newTargetItem);
        }

        /// <summary>
        /// Raises the CollectionChanged event.
        /// </summary>
        /// <param name="e"></param>
        private void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            var collectionChanged = CollectionChanged;
            if (collectionChanged != null) collectionChanged(this, e);
        }

        /// <summary>
        /// Sets the source items.
        /// </summary>
        /// <param name="newSourceItems"></param>
        private void SetItems(TSource[] newSourceItems)
        {
            _TargetList.Clear();
            foreach (TSource newSourceItem in newSourceItems) AddSourceItem(newSourceItem);

            // Remove target items whose corresponding source items have been removed.
            foreach (TSource sourceItem in _SourceToTargetItemMap.Keys.Except(newSourceItems).ToArray())
            {
                _SourceToTargetItemMap.Remove(sourceItem);
            }

            RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        private readonly List<TTarget> _TargetList = new List<TTarget>();
        private readonly Func<TSource, TTarget> _Mapping = null;
        private readonly Dictionary<TSource, TTarget> _SourceToTargetItemMap = new Dictionary<TSource, TTarget>();
    }
}
